Skip to content

fix(codex): see a native draft before writing into it; restart into an empty 0.157 composer (#800, #1313) - #1319

Merged
Juliusolsson05 merged 13 commits into
mainfrom
fix/codex-composer-occupancy
Sep 27, 2026
Merged

Juliusolsson05 merged 13 commits into
mainfrom
fix/codex-composer-occupancy

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1313. Refs #800 (not closed: its remaining case is #1327). Residual: #1327.

Plan: docs/plans/2026-09-25-codex-composer-occupancy.md (first commit, kept current).

Why

Evidence is the raw PTY recording of codex-cli 0.157.0 in codex-headless (testing/fixtures/composer-0157/idle-draft-ctrlc.json).

What merges

Package bump. packages/codex-headless goes ae0e935 → 6334d5ac, the merge of codex-headless#55. That includes #53, #54 and #55:

  • getComposerState() returns empty, drafted or unknown, from cell attributes and Codex's own hint row.
  • getSettledScreen() returns the plain viewport only for a settled frame, and null otherwise. Settled means no chunk is queued in the parser, no synchronized update is open, and Codex has painted since the last resize.

The module is resolved from source, so no lockfile resync is needed.

One rule. drafted is occupied. A proof of empty is ready. Anything else never consents and never latches occupied. A proof of empty is either the package's empty, or a bare › above the status row read from the settled screen, never getScreen().

  • Write gate:

    • drafted refuses with retry-after-resolve and writes nothing.
    • Unclassifiable text is refused at once.
    • A text-only proof must hold on two settled polls 50 ms apart, and a null settled screen resets the count.
  • Restart: the final empty check applies the same proof on its own, even after readiness said ready.

  • Readiness publication:

    Frame Published
    drafted composer-occupied
    proof of empty ready
    anything else provider-not-ready

    nativeDraft reports occupied, and the pane says "draft in agent composer — clear it to send".

  • Agent Code's composer while composer-occupied, for every provider:

    • Enter and Send refuse, keep the draft, and say how to clear the agent's draft.
    • Ctrl+C reaches the agent, because that is how 0.157 clears a draft, and it keeps the Agent Code draft.
  • Tool text: the orchestration hold text and the nativeDraft doc mention a native draft.

Tests

codexSession.nativeComposer.test.ts (9 tests) replays the recorded bytes through a real CodexHeadless:

  • Restart into the idle composer is delivered.
  • Delivery into the draft is refused with no write.
  • composer-occupied is published, then ready after Ctrl+C.
  • A 13-row unclassifiable draft is never written into, and never latches.
  • One stale bare-marker read does not consent.
  • A bare marker with no settled frame never consents, for the gate or the restart.
  • A hintless pane returns to ready only from a settled bare marker.
  • The restart's final check refuses a draft after readiness said ready.

Other suites:

  • codexSession.launchOrdering.test.ts: publication through the real screen listener.
  • useComposerKeybinds.tab.renderer.test.tsx (11 tests): occupied Enter refuses for text, image-only, and a non-Codex provider; Ctrl+C passes and keeps our draft.

Red baseline. These tests are red against main's application code with the new package: restart refused, draft delivered, no publication. On exact main they throw earlier, because the old package has no snapshotComposerCells.

Each new rule was mutation-checked, and every mutant listed in the review disposition is killed.

Verification

  • Codex runtime suites: 24 files, 130 tests.
  • Renderer keybinds: 11 tests.
  • npx tsc -b is clean.
  • Full local suite: 7241 passed, 2 failed, both local-environment only:
    • workflows/control.system needs a built packages/workflow-mcp/dist, which this worktree lacks. It passes in the main checkout.
    • claudeHistory.system passes when run alone.
  • CI: green at b3971a7e (origin/main d15a9756 merged in): quality-gate and minimum-node-fixture-gate pass. Codex suites + keybinds 142/142, npx tsc -b clean after the merge.

Residuals

  • Codex: Enter from Agent Code's composer can still append to a native draft the pane cannot classify #1327: Enter from Agent Code's composer still writes into a native draft the pane cannot classify, because that draft publishes provider-not-ready. Needs a separate "unreadable draft" reason. Orchestration already refuses this case.
  • Readiness can blink while Codex streams on a hintless pane. This is display-only.
  • A keystroke Codex has read but not yet painted is invisible to any terminal. The two-settled-poll rule covers the recorded gap (~20 ms).

Note on #1317

#1317 (the #53 bump alone) is merged. This PR's pointer contains it.

🤖 Generated with Claude Code

Juliusolsson05 and others added 5 commits September 25, 2026 10:19
…1313)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n empty 0.157 composer (#800, #1313)

Bumps codex-headless to 1ac3c50 (#53 base64 proxy chunks, #54
getComposerState). A normal delivery into a human's draft is refused as
occupied instead of appending to it; a draft publishes composer-occupied
and its clearing publishes ready; a restart request consents on Codex's own
empty-composer reading, which the text check could never give on 0.157.
Tests replay the recorded 0.157 bytes through a real CodexHeadless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A draft longer than the package's composer bound read `unknown`, the screen
check still passed, and a delivery pasted into the human's draft. Ready now
needs a proof of empty (the package's reading or the old bare-marker text
proof); text that nothing proves empty is refused at once as a human's to
resolve, and readiness withdraws ready as provider-not-ready without
latching occupied. Tests use steering's 13-row shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…two polls

The plain screen can lag PTY bytes still being parsed, which is exactly
when the cell reading says `unknown`; one stale bare-marker read consented
and a normal delivery pasted into a draft the human had just started. The
text proof now has to hold on two consecutive polls. Main merged in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 26, 2026
…ntinue, #1319 fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ative draft; Ctrl+C clears it

Review C: a Codex submit is one raw PTY write, so while composer-occupied,
Enter in Agent Code's composer pasted the prompt after the native draft and
Codex submitted both. Submit now refuses while occupied (every provider),
keeping the draft, and Ctrl+C reaches the agent, since that is how 0.157
clears a draft. Review B: publication is pinned through the real screen
listener, and the restart's final empty check has its own test. The
orchestration hold and the nativeDraft tool text now mention a native draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 2 commits September 26, 2026 16:57
…trl+C keeps our draft

A2: the text proof of an empty composer read the plain screen, which shows
the previous paint while PTY bytes are still being parsed, and two polls do
not prove the parser caught up. The write gate, readiness publication and the
restart's final check now read the settled screen (codex-headless#55), null
while anything is unparsed; null waits or refuses, never consents.

B: Ctrl+C while the agent's composer is occupied clears the AGENT's draft
only; it no longer deletes the follow-up typed in Agent Code. Pins the
image-only occupied refusal too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settled reads now also refuse inside an open synchronized update and after an
unredrawn resize, and count a chunk before pty-data listeners run. Interim
pointer; the final bump goes to #55's merge commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 4 commits September 26, 2026 17:23
…s provider-neutral

A guard narrowed to provider === 'codex' survived every test. Claude also
publishes composer-occupied and submits with a raw write, so it is pinned with
a Claude pane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y from the settled text proof

Deleting the publication's text fallback passed every test. Pins that a pane
with no shortcuts hint (package says unknown) publishes ready from a settled
bare marker, and stays provider-not-ready while the settled read is null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ounds

unknown is no longer 'today's behaviour': it never consents and publishes
provider-not-ready. Records the settled screen, the pane's own Enter/Ctrl+C
rules, the full test list, and the residuals (#1327).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4d5ac)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review disposition (final head c58234aa)

Two rounds, three reviewers each. Round 2: C MERGE-READY; A and B FIX-BEFORE-MERGE, both addressed below.

Round 1

  • A1 blocker (unknown mapped to ready, so a 13-row draft was pasted into): fixed in f6fce736. Ready needs a proof of empty. Unclassifiable text is refused and publishes provider-not-ready.
  • A2 major (a stale bare-marker text proof consented): fixed in e8490f29 (two polls) and 9878c35d (the proof reads the settled screen, codex-headless#55).
  • A3 major (composer-occupied latched after a narrow-pane clear): fixed in f6fce736. Anything not drafted leaves occupied.
  • B major (publication was not driven through the screen listener): fixed in 4b8e9729 (launchOrdering test).
  • B minor (the tests were red on exact main for a setup error): valid; the claim is corrected, not the helper. On exact main they throw before reaching behavior, because the old package has no snapshotComposerCells. The body now states the baseline honestly: new package plus main's application code, which fails on the intended behavior (restart refused, draft delivered, no publication). A main-compatible replay helper would be scaffolding to prove a claim, not behavior. Declined.
  • B minor (the final empty check was untested alone): fixed in 4b8e9729.
  • C blocker (the pane's own Enter pasted after a native draft): fixed in 4b8e9729. Submit refuses while composer-occupied, for every provider.
  • C major (Ctrl+C was eaten while occupied): fixed in 4b8e9729.
  • C minors (the hold text and the nativeDraft tool text did not mention a draft): fixed in 4b8e9729.

Round 2

  • A1 blocker (Enter still writes into a draft the pane cannot classify, which publishes provider-not-ready): accepted residual, follow-up Codex: Enter from Agent Code's composer can still append to a native draft the pane cannot classify #1327. Reviewer C agrees. The pane has one reason for every unverified frame. Refusing all provider-not-ready would also refuse queueing a prompt during a turn, which is the ordinary Enter path. The fix is a separate "unreadable draft" reason. Orchestration delivery already refuses this case.
  • A2 major (two text polls can both read a stale frame while bytes are unparsed): fixed in 9878c35d plus codex-headless#55.
  • B major (Ctrl+C while occupied deleted the user's Agent Code draft): fixed in 9878c35d.
  • B minor (the warning row was not pinned in the package): fixed in codex-headless#55.
  • B minor (two untested fallback branches):
    • Image-only occupied Enter: fixed in 9878c35d.
    • The hintless-pane text fallback publishing ready: fixed in dc03d23f. Both the "fallback removed" and "fallback reads the unsettled screen" mutants are killed.
  • B surviving mutants (the exit reset, and the composerReady/exited publication guard): declined. The exit reset is redundant with the next start's reset, as the reviewer notes. The guard only stops publication before startup or after exit, and neither state has a screen event in the recordings. Pinning it would be a vanity test.
  • C (a guard narrowed to provider === 'codex' survived): fixed in 6aee6e6d, pinned with a Claude pane.
  • C (readiness flicker while streaming): accepted. It is display-only; see the residuals in the plan.

@Juliusolsson05 Juliusolsson05 added type:bug Something works wrong class:C1-prompt-delivery Prompt delivery inferred from the terminal screen class:C4-input-trap A modal/latch/overlay traps input sev:P2 Real bug with a workaround provider:codex Affects Codex integration labels Sep 27, 2026
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Verification addendum: round-2 reviewers A and B each did one focused verification pass at b3971a7e (with codex-headless at merged #55 6334d5ac). Both MERGE-READY. Exact-head CI is green and the branch contains main d15a9756. Merging.

@Juliusolsson05
Juliusolsson05 merged commit b3a2c48 into main Sep 27, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/codex-composer-occupancy branch September 27, 2026 01:28
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

class:C1-prompt-delivery Prompt delivery inferred from the terminal screen class:C4-input-trap A modal/latch/overlay traps input provider:codex Affects Codex integration sev:P2 Real bug with a workaround type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(codex): browser-pocket server restart is always refused on Codex 0.157 — the empty-composer check cannot see an empty composer

1 participant